Tech

Diary

Lecture

About Me

개발중

FE 통합 테스트 한계

JeongSeulho

2024년 01월 08일

준비중...
클립보드로 복사

0. 들어가며

배송 주문 페이지를 예시로 통합 테스트를 작성하고 그 한계를 정리

1. ItemList 컴포넌트 통합 테스트

copy
import { screen, within } from '@testing-library/react';
import React from 'react';

import ItemList from '@/pages/purchase/components/ItemList';
import { mockUseCartStore } from '@/utils/test/mockZustandStore';
import render from '@/utils/test/render';

beforeEach(() => {
  mockUseCartStore({
    cart: {
      6: {
        id: 6,
        title: 'Handmade Cotton Fish',
        price: 100,
        description:
          'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
        images: [
          'https://user-images.githubusercontent.com/35371660/230712070-afa23da8-1bda-4cc4-9a59-50a263ee629f.png',
        ],
        count: 3,
      },
      7: {
        id: 7,
        title: 'Awesome Concrete Shirt',
        price: 50,
        description:
          'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
        images: [
          'https://user-images.githubusercontent.com/35371660/230762100-b119d836-3c5b-4980-9846-b7d32ea4a08f.png',
        ],
        count: 4,
      },
    },
    totalCount: 7,
    totalPrice: 500,
  });
});

it('구매 상품들의 이름, 수량, 금액이 순서대로 노출된다.', async () => {
  await render(<ItemList />);

  const rows = screen.getAllByRole('row');
  const first = within(rows[0]);
  const second = within(rows[1]);

  expect(first.getByText('Handmade Cotton Fish')).toBeInTheDocument();
  expect(first.getByText('3개')).toBeInTheDocument();
  expect(first.getByText('$300.00')).toBeInTheDocument();

  expect(second.getByText('Awesome Concrete Shirt')).toBeInTheDocument();
  expect(second.getByText('4개')).toBeInTheDocument();
  expect(second.getByText('$200.00')).toBeInTheDocument();
});

2. Payment 컴포넌트 통합 테스트

copy
import { screen } from '@testing-library/react';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';

import { NO_COUPON_ID } from '@/constants';
import Payment from '@/pages/purchase/components/Payment';
import { mockUseCartStore } from '@/utils/test/mockZustandStore';
import render from '@/utils/test/render';

beforeEach(() => {
  mockUseCartStore({
    cart: {
      6: {
        id: 6,
        title: 'Handmade Cotton Fish',
        price: 100,
        description:
          'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
        images: [
          'https://user-images.githubusercontent.com/35371660/230712070-afa23da8-1bda-4cc4-9a59-50a263ee629f.png',
        ],
        count: 3,
      },
      7: {
        id: 7,
        title: 'Awesome Concrete Shirt',
        price: 50,
        description:
          'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
        images: [
          'https://user-images.githubusercontent.com/35371660/230762100-b119d836-3c5b-4980-9846-b7d32ea4a08f.png',
        ],
        count: 4,
      },
    },
    totalCount: 7,
    totalPrice: 500,
  });
});

// react-hook-form 검증을 위해 FormProvider를 랩핑한 컴포넌트 생성
const TestPayment = (props = {}) => {
  const methods = useForm({
    defaultValues: {
      name: 'leejaesung',
      address: '',
      phone: '',
      requests: '',
      coupon: NO_COUPON_ID,
      ...props,
    },
  });

  return (
    <FormProvider {...methods}>
      <Payment />
    </FormProvider>
  );
};

it('총 상품 금액은 "$500.00"로 노출된다', async () => {
  await render(<TestPayment />);

  expect(screen.getByText('$500.00')).toBeInTheDocument();
});

it('배송비는 "$5.00"로 노출된다', async () => {
  await render(<TestPayment />);

  expect(screen.getByText('$5.00')).toBeInTheDocument();
});

it('할인 쿠폰을 선택하지 않은 경우 "선택 안함"으로 노출되며, 총 결제 금액은 "$505.00"로 노출된다', async () => {
  await render(<TestPayment />);

  expect(screen.getByText('선택 안함')).toBeInTheDocument();
  expect(await screen.findByText('$505.00')).toBeInTheDocument();
});

it('price 타입의 쿠폰인 경우, 총 결제 금액은 "$502.00"로 노출된다', async () => {
  await render(<TestPayment coupon={2} />);

  expect(await screen.findByText('$3 할인 쿠폰')).toBeInTheDocument();
  expect(await screen.findByText('$502.00')).toBeInTheDocument();
});

it('percent 타입의 쿠폰인 경우, 총 결제 금액은 "$455.00"로 노출된다', async () => {
  await render(<TestPayment coupon={3} />);

  expect(await screen.findByText('10% 할인 쿠폰')).toBeInTheDocument();
  expect(await screen.findByText('$455.00')).toBeInTheDocument();
});

3. ShippingInformationForm 컴포넌트 통합 테스트

copy
import { screen } from '@testing-library/react';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';

import { NO_COUPON_ID } from '@/constants';
import ShippingInformationForm from '@/pages/purchase/components/ShippingInformationForm';
import render from '@/utils/test/render';

const TestForm = props => {
  const methods = useForm({
    defaultValues: {
      name: '',
      address: '',
      phone: '',
      requests: '',
      coupon: NO_COUPON_ID,
      ...props,
    },
  });

  return (
    <FormProvider {...methods}>
      <ShippingInformationForm />
      <button type="button" onClick={methods.handleSubmit(() => {})}>
        테스트 버튼
      </button>
    </FormProvider>
  );
};

it('쿠폰 데이터를 가져오면 정상적으로 쿠폰 항목을 노출한다.', async () => {
  const { user } = await render(<TestForm />);

  const [selectBoxButton] = await screen.findAllByRole('button');

  await user.click(selectBoxButton);

  expect(screen.getByText('가입 기념! $5 할인 쿠폰')).toBeInTheDocument();
  expect(screen.getByText('$3 할인 쿠폰')).toBeInTheDocument();
  expect(screen.getByText('10% 할인 쿠폰')).toBeInTheDocument();
});

it('이름을 입력하지 않고 폼 전송을 시도하면 "이름을 입력하세요" 텍스트가 노출된다.', async () => {
  const { user } = await render(<TestForm />);

  const testSubmitButton = await screen.findByText('테스트 버튼');
  await user.click(testSubmitButton);

  expect(screen.getByText('이름을 입력하세요')).toBeInTheDocument();
});

it('주소를 입력하지 않고 폼 전송을 시도하면 "주소를 입력하세요" 텍스트가 노출된다.', async () => {
  const { user } = await render(<TestForm />);

  const testSubmitButton = await screen.findByText('테스트 버튼');
  await user.click(testSubmitButton);

  expect(screen.getByText('주소를 입력하세요')).toBeInTheDocument();
});

it('휴대폰 번호를 입력하지 않고 폼 전송을 시도하면 "휴대폰 번호를 입력하세요" 텍스트가 노출된다.', async () => {
  const { user } = await render(<TestForm />);

  const testSubmitButton = await screen.findByText('테스트 버튼');
  await user.click(testSubmitButton);

  expect(screen.getByText('휴대폰 번호를 입력하세요')).toBeInTheDocument();
});

it('휴대폰 번호의 패턴이 틀린 상태에서 폼 전송을 시도하면 "휴대폰 번호를 입력하세요" 텍스트가 노출된다.', async () => {
  const { user } = await render(<TestForm phone="01099999999" />);

  const testSubmitButton = await screen.findByText('테스트 버튼');
  await user.click(testSubmitButton);

  expect(
    screen.getByText('-를 포함한 휴대폰 번호만 가능합니다'),
  ).toBeInTheDocument();
});

4. 베송 주문 페이지의 통합 테스트 한계

배송 주문 페이지에서 가장 중요한 테스트는

  1. 구매하기 버튼을 눌렀을 때, 올바른 주문서 데이터가 만들어 지는지
  2. 이후 결제 성공 또는 실패 시, 올바르게 처리되는지

하지만, 이 두 가지 테스트를 통합 테스트로 작성하면 많은 모킹이 필요하다.
각 컴포넌트의 스토어, API 모킹 뿐만 아니라 구매하기 버튼에 따른 성공, 다양한 실패 케이스에 따른 API 응답까지 모두 모킹해야 한다.
또한, 결제 방법에 따라 빌링 프로세스가 다르다면 이 또한 모두 모킹해야 한다.
이런 너무 많은 모킹은 빼먹는 시나리오가 생길 수 있고, 모킹에 의존적이므로 신뢰도가 떨어진다.
이를 위해 E2E 테스트가 필요하다.